// --- Copyright (c) 2006-2008 Stefan Kyntchev ---
// This software is written and copyrighted by Stefan Kyntchev 
// and BeyondPod Team members. All rights are reserved.
// Author contact: support@beyondpod.mobi
// ------------------------------------------------------------------------
// This file is part of BeyondPod RSS Feed Reader and Podcast manager
// (www.codeplex.com/BeyondPod) 
// BeyondPod is free software: you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published by 
// the Free Software Foundation, either version 3 of the License, or 
// (at your option) any later version. 
//  
// BeyondPod is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
// GNU General Public License for more details. 
//  
// You should have received a copy of the GNU General Public License
// along with BeyondPod. If not, see <http://www.gnu.org/licenses/>
// ------------------------------------------------------------------------
// COPYRIGHT NOTICE: This copyright notice may NOT be removed, obscured or modified 
// without written consent from the author.
// --- End copyright notice --- 


using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Xml;
using svs.Mobile.BeyondMedia.FeedCore.Helpers;
using svs.Mobile.BeyondMedia.FeedCore.Properties;
using svs.Mobile.BeyondMedia.UiCore;

namespace svs.Mobile.BeyondMedia.FeedCore
{
	public static class FeedRepository
	{
		private static readonly Feed m_GoUpPlaceholder = new Feed(Guid.NewGuid(), m_RootFeed) { Name = ".." };
		private static readonly Feed m_RootFeed = new Feed(Guid.Empty, null);
		private static readonly TrackDictionary m_TrackDictionary = new TrackDictionary();
		private static bool m_IsRepositoryLoading;
		private static DateTime m_LastSavedTime = DateTime.MaxValue;
		private static string m_RootPath;

		#region Events

		public static event CoreHelper.TrackBeforeDeletingEvent BeforeTrackDelete;
		public static event CoreHelper.TrackDeletedEvent TrackDeleted;
		public static event CoreHelper.TrackAddedEvent TrackAdded;
		public static event CoreHelper.TrackChangedEvent TrackChanged;
		public static event CoreHelper.FeedChangedEvent FeedChanged;
		public static event CoreHelper.RepositoryLoadedEvent RepositoryLoaded;
		public static event CoreHelper.RepositorySaveEvent RepositorySave;

		#endregion

		#region Constructors

		static FeedRepository()
		{
			ReloadRepository();
		}

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets or sets the root path of the repository.
		/// </summary>
		/// <value>The root path.</value>
		public static string RootPath
		{
			get { return m_RootPath; }
			set
			{
				if (value.IndexOf(@"/") >= 0)
					m_RootPath = value.Replace("/", @"\");
				else
					m_RootPath = value;

				m_RootFeed.FeedPath = m_RootPath;
			}
		}

		public static bool IsEmpty
		{
			get { return RootFeed.Feeds.Count == 0; }
		}

		public static Feed RootFeed
		{
			get { return m_RootFeed; }
		}


		public static Feed GoUpPlaceholder
		{
			get { return m_GoUpPlaceholder; }
		}

		public static int FeedCount
		{
			get { return RootFeed.Feeds.Count; }
		}

		public static int TrackCount
		{
			get { return m_TrackDictionary.Count; }
		}

		public static string RepositoryFilePath
		{
			get { return Configuration.TrackStatePath; }
		}

		public static string BackupRepositoryFilePath
		{
			get { return RepositoryFilePath + ".bak"; }
		}

		public static bool IsRepositoryLoading
		{
			get { return m_IsRepositoryLoading; }
			set
			{
				//Publish an event when the repository is done loading
				if (m_IsRepositoryLoading && value == false)
				{
					if (RepositoryLoaded != null)
						RepositoryLoaded();
				}

				m_IsRepositoryLoading = value;
			}
		}

		#endregion

		#region Public Methods

		public static void SaveRepositoryAsOpml(string file)
		{
			lock (m_TrackDictionary)
			{
				XmlTextWriter tw = null;
				try
				{
					tw = new XmlTextWriter(file, Encoding.UTF8);
					tw.WriteStartDocument();

					//<opml version="1.1">
					tw.WriteStartElement("opml");
					tw.WriteAttributeString("version", "1.1");

					tw.WriteStartElement("head");
					tw.WriteStartElement("title");
					tw.WriteString("BeyondPod Feeds");
					tw.WriteEndElement(); //title

					tw.WriteStartElement("dateCreated");
					tw.WriteString(DateTime.Now.ToString("r")); //Mon, 10 Oct 2005 08:51PM PDT
					tw.WriteEndElement();

					tw.WriteStartElement("dateModified");
					tw.WriteString(DateTime.Now.ToString("r"));
					tw.WriteEndElement();

					tw.WriteEndElement(); //</head>

					tw.WriteStartElement("body");

					foreach (FeedCategory category in CategoryManager.GetCategoriesForAssignment())
					{
						tw.WriteStartElement("outline");
						tw.WriteAttributeString("text", category.Name);

						foreach (Feed feed in RootFeed.Feeds)
						{
							if (feed.HasUrl && feed.Categories.Primary == category)
							{
								tw.WriteStartElement("outline");
								tw.WriteAttributeString("text", feed.Name);
								tw.WriteAttributeString("xmlUrl", feed.FeedUrl);
								tw.WriteAttributeString("type", "rss");
								tw.WriteEndElement();
							}
						}
						tw.WriteEndElement();
					}

					tw.WriteEndElement(); //body
					tw.WriteEndElement(); //opml
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry(string.Format("Unable to export to file: {0}Reason: {1}", file, ex.Message));
					throw;
				}
				finally
				{
					if (tw != null)
						tw.Close();
				}
			}
		}

		public static void SaveRepository()
		{
			// lock the track dictionary
			Monitor.Enter(m_TrackDictionary);

			try
			{
				if (RepositorySave != null)
				{
					RepositorySave(true);
				}

				//First Write to a temp file so we don't loose the real one if somethig happens
				CoreHelper.WriteTraceEntry("Repository save started...");
				XmlTextWriter tw = null;

				try
				{
					tw = new XmlTextWriter(BackupRepositoryFilePath, Encoding.UTF8);

					tw.WriteStartDocument();

					tw.WriteStartElement("store");

					tw.WriteStartElement("categories");

					tw.WriteString(CategoryManager.GetSerializedCategories());

					tw.WriteEndElement();

					//Save the tracks
					tw.WriteStartElement("tracks");

					foreach (Track t in m_TrackDictionary.Values)
					{
						if (t.IsModified)
						{
							tw.WriteStartElement("track");
							tw.WriteAttributeString("path", t.TrackPath);
							tw.WriteAttributeString("totalTime", DoubleToInvariantCultureString(t.TotalTime));
							tw.WriteAttributeString("playedTime", DoubleToInvariantCultureString(t.PlayedTime));
							tw.WriteAttributeString("name", t.Name);
							tw.WriteAttributeString("played", t.IsPlayed ? "1" : "0");
							tw.WriteAttributeString("protocol", t.Protocol);
							tw.WriteAttributeString("url", t.Url);
							tw.WriteAttributeString("downloadSize", LongToInvariantCultureString(t.DownloadSize));
							tw.WriteAttributeString("downloadPortion", LongToInvariantCultureString(t.DownloadedPortion));
							tw.WriteAttributeString("pubDate", LocalDateTimeToGMTString(t.LastModifiedDate));
							tw.WriteEndElement();
						}
					}

					//tracks
					tw.WriteEndElement();

					//Save the feeds
					tw.WriteStartElement("feeds");

					foreach (Feed f in RootFeed.Feeds)
					{
						if (f.IsModified)
						{
							tw.WriteStartElement("feed");
							tw.WriteAttributeString("id", f.ID.ToString());
							tw.WriteAttributeString("path", f.GetRawFeedPath());
							tw.WriteAttributeString("name", f.Name);
							tw.WriteAttributeString("autodelete", ((int)f.AllowAutoTrackDeletions).ToString());
							tw.WriteAttributeString("view", ((int)f.ViewType).ToString());
							tw.WriteAttributeString("type", ((int)f.Type).ToString());
							tw.WriteAttributeString("url", f.FeedUrl);
							tw.WriteAttributeString("imageUrl", f.FeedImageUrl);
							tw.WriteAttributeString("isNew", f.HasNewContent ? "1" : "0");
							tw.WriteAttributeString("hasUnread", f.HasUnreadItems ? "1" : "0");
							tw.WriteAttributeString("pubDate", LocalDateTimeToGMTString(f.LastModifiedDate));
							tw.WriteAttributeString("srvPubDate", LocalDateTimeToGMTString(f.LastServerModifiedDate));
							tw.WriteAttributeString("category", f.Categories.Serialize());
							tw.WriteAttributeString("custDownload", f.IncludeInOneStepDownload ? "1" : "0");
							tw.WriteAttributeString("forceUniqueNames", f.ForceUniqueTrackNames ? "1" : "0");
							tw.WriteAttributeString("leftTruncateLongTrackNames", f.LeftTruncateLongTrackNames ? "1" : "0");
							tw.WriteAttributeString("forceItemSort", f.ForceFeedItemSort ? "1" : "0");

							if (f.LastItemID.HasValue)
								tw.WriteAttributeString("lastItemId", f.LastItemID.ToString());

							if (f.TrackSortOrder != Configuration.TracksSortOrder)
								tw.WriteAttributeString("trackSort", ((int)f.TrackSortOrder).ToString());

							tw.WriteAttributeString("feedPlayer", ((int)f.FeedPlayer).ToString());

							if (!string.IsNullOrEmpty(f.UserName))
								tw.WriteAttributeString("username", f.UserName);

							if (!string.IsNullOrEmpty(f.Password))
								tw.WriteAttributeString("password", f.Password);

							if (f.NumToIncludeInWhatIsNew != Configuration.GlobalNumItemsToIncludeInWhatIsNew)
								tw.WriteAttributeString("numWhatIsNew", f.NumToIncludeInWhatIsNew.ToString());

							if (f.MaxEnclosuresToOneStepDownload != Configuration.GlobalMaxEnclosuresToOneStepDownload)
								tw.WriteAttributeString("maxDownload", f.MaxEnclosuresToOneStepDownload.ToString());

							if (f.MaxEnclosures != Configuration.GlobalMaxEnclosures)
								tw.WriteAttributeString("maxTracks", f.MaxEnclosures.ToString());

							if (f.KeepEnclosuresDuration != Configuration.GlobalKeepEnclosuresDuration)
								tw.WriteAttributeString("maxTrackAge", ((int)f.KeepEnclosuresDuration.TotalDays).ToString());

							tw.WriteEndElement();
						}
					}

					//feeds
					tw.WriteEndElement();

					//store
					tw.WriteEndElement();

					tw.Close();
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry("Unable to save feed repository! " + ex.Message);
				}
				finally
				{
					if (tw != null)
						tw.Close();
				}

				//Now replace the real file with the temp file
				try
				{
					File.Copy(BackupRepositoryFilePath, RepositoryFilePath, true);
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry("Unable to copy temp repository file! " + ex.Message);
				}

				m_LastSavedTime = DateTime.Now;
				CoreHelper.WriteTraceEntry("Repository save completed!");
			}
			finally
			{
				if (RepositorySave != null)
				{
					// because the notification is no scheduled we can call it before releasing the lock.
					RepositorySave(false);
				}
				Monitor.Exit(m_TrackDictionary);
			}
		}

		/// <summary>
		/// Retrieves all feeds that are in a given category
		/// </summary>
		/// <param name="category"></param>
		/// <returns></returns>
		public static FeedList GetFeedsInCategory(FeedCategory category)
		{
			var retVal = new FeedList();

			foreach (Feed feed in RootFeed.Feeds)
			{
				if (category == CategoryManager.AllFeeds || (feed.Categories != null &&
																										 feed.Categories.IsOfCategory(category)))
					retVal.Add(feed);
			}

			return retVal;
		}

		public static Feed GetFeedByPath(string p)
		{
			foreach (Feed f in RootFeed.Feeds)
			{
				if (f.FeedPath == p)
				{
					return f;
				}
			}

			return null;
		}

		public static Feed GetFeedById(Guid id)
		{
			foreach (Feed f in RootFeed.Feeds)
			{
				if (f.ID.Equals(id))
				{
					return f;
				}
			}

			return null;
		}

		public static Feed GetFeedByUrl(string uri)
		{
			foreach (Feed f in RootFeed.Feeds)
			{
				if (string.Compare(f.FeedUrl, uri, StringComparison.InvariantCultureIgnoreCase) == 0)
				{
					return f;
				}
			}

			return null;
		}

		public static Track GetTrackByPath(string path)
		{
			Track t = null;

			if (path.IndexOf(@"/") >= 0)
				path = path.Replace("/", @"\");

			foreach (var pair in m_TrackDictionary)
			{
				if (string.Compare(pair.Key, path, StringComparison.InvariantCultureIgnoreCase) == 0)
					t = pair.Value;
			}

			return t;
		}


		public static TrackList GetAllTracks()
		{
			var retVal = new TrackList();
			AppendTracks(RootFeed, retVal);
			return retVal;
		}

		public static void DeleteAllPlayedTracks(Feed i_Feed)
		{
			var toDelete = new TrackList();

			foreach (Track t in m_TrackDictionary.Values)
			{
				if (t.AllowAutoDelete && t.IsPlayed && (i_Feed == null || t.ParentFeed == i_Feed))
				{
					toDelete.Add(t);
				}
			}

			DeleteTracksInternal(toDelete);
		}

		public static void Refresh()
		{
			SaveRepository();
			ReloadRepository();
		}

		public static void DeleteTrack(Track track, bool silent)
		{
			if (!silent)
			{
				if (MessageBox.Show(string.Format(Resources.AboutToDeleteFileMsg, track.TrackPath), Resources.AboutToDeleteFileTitle,
														MessageBoxButtons.YesNo, MessageBoxIcon.Question,
														MessageBoxDefaultButton.Button1) == DialogResult.No)
					return;
			}

			DeleteTrackInternal(track, silent);
		}

		public static void DeleteOldTracks()
		{
			DeleteTracksInternal(GetOldTracks());
		}

		public static void DeleteOldTracksForFeed(Feed feed, bool makeSpace)
		{
			TrackList t = GetOldTracksForFeed(feed, makeSpace);
			foreach (Track track in t)
			{
				DeleteTrackInternal(track, true);
			}
		}

		public static Track FindOrCreateLocalTrack(Feed feed, string Uri, string mimetype, DateTime? lastModifiedDate)
		{
			//Make sure we have a place to download the podcast
			if (!EnsureFeedHasDownloadPath(feed))
				return null;

			//Construct the name of the file in that folder
			string downloadFile = Path.Combine(feed.FeedPath,
																				 CoreHelper.CreateFileNameFromUrl(Uri, mimetype, feed.ForceUniqueTrackNames, feed.Name));

			//Try to find if we alredy have started the download
			Track t = GetTrackByPath(downloadFile);

			if (t == null)
			{
				//This is a new download
				t = new Track(downloadFile, feed)
							{
								Url = Uri,
								Protocol = "rss",
								LastModifiedDate = lastModifiedDate ?? DateTime.Now
							};

				AddChildTrack(feed, t, true);
			}

			//Make sure the file is not already downloaded
			if (t.IsFullyDownloaded)
			{
				MessageBox.Show(Resources.EnclosureAlreadyDownloadedMsg, Resources.EnclosureAlreadyDownloadedTitle,
												MessageBoxButtons.OK,
												MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);
				return null;
			}

			//Now we have a Track to download
			return t;
		}


		public static bool EnsureFeedHasDownloadPath(Feed feed)
		{
			//First ensure we have a place to download
			if (!Directory.Exists(feed.FeedPath))
			{
				//Check if we have the root directory set correctly
				if (!RootFeed.DownloadPathExists)
				{
					MessageBox.Show(Resources.IvalidFeedsRootFolderMsg, Resources.IvalidFeedsRootFolderTitle, MessageBoxButtons.OK,
													MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);
					return false;
				}

				//Try to create a directory using the root path and the feed name
				try
				{
					string path = Path.Combine(RootPath, CoreHelper.MakeValidFileName(feed.Name));
					Directory.CreateDirectory(path);
					feed.FeedPath = path;
				}
				catch
				{
					MessageBox.Show(Resources.InvalidFeedDownloadFolderMsg, Resources.InvalidFeedDownloadFolderTitle,
													MessageBoxButtons.OK,
													MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);
					return false;
				}
			}

			return true;
		}

		/// <summary>
		/// Deletes a Feed
		/// </summary>
		/// <param name="feed"></param>
		public static void DeleteFeed(Feed feed)
		{
			RootFeed.Feeds.Remove(feed);
			foreach (Track track in feed.Tracks)
			{
				m_TrackDictionary.Remove(track.TrackPath);
			}
		}

		public static void SortTracks(Feed i_Feed)
		{
			i_Feed.Tracks.Sort(new TrackComparer<Track>(i_Feed.TrackSortOrder));
		}

		public static void SortFeeds(Feed i_Feed)
		{
			i_Feed.Feeds.Sort(new FeedComparer<Feed>(Configuration.FeedsSortOrder));
		}

		public static void SortTrackUsingDefaultSettings(TrackList tracks)
		{
			tracks.Sort(new TrackComparer<Track>(Configuration.TracksSortOrder));
		}

		public static void SortTrackUsingDefaultSettingsGroupedByFeed(TrackList tracks)
		{
			var feeds = new FeedList();

			//First sort all tracks
			tracks.Sort(new TrackComparer<Track>(Configuration.TracksSortOrder));

			//Find all track's feeds and sort them
			foreach (var track in tracks)
			{
				if (!feeds.Contains(track.ParentFeed))
					feeds.Add(track.ParentFeed);
			}

			var sorted = new TrackList();

			//Now sort the feeds
			feeds.Sort(new FeedComparer<Feed>(Configuration.FeedsSortOrder));

			//Pull all tracks based on their feed and sort them based on the sort order for that feed
			foreach (var feed in feeds)
			{
				var feedBucket = new TrackList();

				foreach (var track in tracks)
				{
					if (track.ParentFeed == feed)
						feedBucket.Add(track);
				}

				feedBucket.Sort(new TrackComparer<Track>(feed.TrackSortOrder));
				sorted.AddRange(feedBucket);
			}

			tracks.Clear();
			tracks.AddRange(sorted);
		}

		public static void AutoSaveRepository()
		{
			//Disable Autosave during repository refresh
			if (IsRepositoryLoading)
				return;

			//Save the repository if it was not saved in the for XX minutes
			if ((DateTime.Now - m_LastSavedTime).TotalMinutes > 4)
			{
				ThreadPool.QueueUserWorkItem(o => SaveRepository());
			}
		}

		public static string GeneratePlaylistFor(TrackList playlist, Track selected, PlayerIntegration playlisType)
		{
			if (playlisType == PlayerIntegration.UseExternalPlayerPlaylistPLS)
				return GeneratePlsPlaylist(playlist, selected);

			return GenerateAsxPlaylist(playlist, selected);
		}

		#endregion

		#region Private Methods

		private static string GenerateAsxPlaylist(TrackList playlist, Track selected)
		{
			StreamWriter stream = null;
			string playlistFile = Path.Combine(Configuration.RssCachePath, "BeyondPod.ASX");
			try
			{
				const string EntryFormat = "<Entry><Title > {0}</Title><Ref href = \"{1}\"/></Entry>";

				stream = File.CreateText(playlistFile);
				stream.WriteLine("<Asx Version = \"3.0\" >");
				stream.WriteLine("<Title >{0}</Title>", selected.ParentFeed.Name);

				int startFrom = playlist.IndexOf(selected);

				if (startFrom != -1)
				{
					for (int c = startFrom; c != playlist.Count; c++)
					{
						Track track = playlist[c];
						stream.WriteLine(string.Format(EntryFormat, track.Name,
																					 track.TrackType == Track.StreamType.InternetStream ? track.Url : track.TrackPath));
					}
				}
				else
				{
					stream.WriteLine(string.Format(EntryFormat, selected.Name,
																				 selected.TrackType == Track.StreamType.InternetStream
																					? selected.Url
																					: selected.TrackPath));
				}

				stream.WriteLine("</Asx>");
			}
			finally
			{
				if (stream != null)
					stream.Close();
			}

			return playlistFile;
		}

		private static void AppendTracks(Feed parent, TrackList list)
		{
			list.AddRange(parent.Tracks);

			if (parent.Feeds.Count > 0)
			{
				foreach (Feed feed in parent.Feeds)
				{
					AppendTracks(feed, list);
				}
			}
		}

		private static string GeneratePlsPlaylist(TrackList playlist, Track selected)
		{
			StreamWriter stream = null;
			string playlistFile = Path.Combine(Configuration.RssCachePath, "BeyondPod.PLS");
			try
			{
				stream = File.CreateText(playlistFile);
				stream.WriteLine("[playlist]");
				int startFrom = playlist.IndexOf(selected);

				int i = 1;
				if (startFrom != -1)
				{
					for (int c = startFrom; c != playlist.Count; c++)
					{
						Track track = playlist[c];
						stream.WriteLine(string.Format("File{0}={1}", i,
																					 track.TrackType == Track.StreamType.InternetStream ? track.Url : track.TrackPath));
						stream.WriteLine(string.Format("Title{0}={1}", i++, track.Name));
						i++;
					}
					stream.WriteLine(string.Format("NumberOfEntries={0}", playlist.Count - startFrom));
				}
				else
				{
					stream.WriteLine(string.Format("File1={0}",
																				 selected.TrackType == Track.StreamType.InternetStream
																					? selected.Url
																					: selected.TrackPath));
					stream.WriteLine(string.Format("Title1={0}", selected.Name));
					stream.WriteLine("NumberOfEntries=1");
				}

				stream.WriteLine("Version=2");
			}
			finally
			{
				if (stream != null)
					stream.Close();
			}

			return playlistFile;
		}

		private static TrackList GetOldTracks()
		{
			var toDelete = new TrackList();

			foreach (Feed feed in RootFeed.Feeds)
			{
				toDelete.AddRange(GetOldTracksForFeed(feed, false));
			}

			return toDelete;
		}

		private static TrackList GetOldTracksForFeed(Feed feed, bool makeSpace)
		{
			var toDelete = new TrackList();

			if (!feed.AllowEnclosures || feed.Tracks.Count == 0 || !feed.CanDeleteTracks)
				return toDelete;

			//Make a copy of the tracks
			var copy = new TrackList();
			copy.AddRange(feed.Tracks);

			//Sort them by date (oldest at the bottom)
			copy.Sort(new GenericComparer<Track>("LastModifiedDate", GenericComparer<Track>.SortOrder.Ascending));

			var feeddelete = new TrackList();

			//Delete all tracks older than our cutoff time
			copy.ForEach(delegate(Track t)
										{
											if (t.LastModifiedDate.HasValue && t.LastModifiedDate < (DateTime.Now - feed.KeepEnclosuresDuration))
											{
												feeddelete.Add(t);
											}
										});

			feeddelete.ForEach(t => copy.Remove(t));

			//Add the old feed for deletion
			toDelete.AddRange(feeddelete);

			//check if we have to make a space for a new track
			int maxAllowed = feed.MaxEnclosures;
			if (makeSpace)
				maxAllowed -= 1;

			//If we still have more than max tracks remove the extra
			if (copy.Count > maxAllowed)
			{
				toDelete.AddRange(copy.GetRange(0, copy.Count - maxAllowed));
			}

			return toDelete;
		}

		private static void ReloadRepository()
		{
			lock (m_TrackDictionary)
			{
				try
				{
					CoreHelper.WriteTraceEntry("Repository load started...");
					Cursor.Current = Cursors.WaitCursor;
					IsRepositoryLoading = true; 

					RootFeed.Feeds.Clear();
					m_TrackDictionary.Clear();

					CoreHelper.KeepDeviceAwake();

					TrackDictionary savedTracks = LoadRepository();

					CoreHelper.KeepDeviceAwake();

					foreach (Feed feed in RootFeed.Feeds)
					{
						BuildFeedList(feed, savedTracks);
						CoreHelper.KeepDeviceAwake();
					}

					SortFeeds(RootFeed);
				}
				finally
				{
					Cursor.Current = Cursors.Default;
					IsRepositoryLoading = false; // this fires the RepositoryLoaded event.
					m_LastSavedTime = DateTime.Now;
					CoreHelper.WriteTraceEntry("Repository load completed!");
				}
			}
		}

		private static TrackDictionary LoadRepository()
		{
			string loadPath = RepositoryFilePath;

			if (!File.Exists(loadPath))
			{
				loadPath = BackupRepositoryFilePath;
				if (!File.Exists(loadPath))
				{
					return null;
				}
			}

			TrackDictionary tracks = null;

			try
			{
				tracks = LoadRepositoryInternal(loadPath);
			}
			catch
			{
				//Retry using the backup
				if (loadPath == RepositoryFilePath && File.Exists(BackupRepositoryFilePath))
				{
					try
					{
						tracks = LoadRepositoryInternal(BackupRepositoryFilePath);
					}
					catch
					{
						// backup failed too. Just exit!
					}
				}
			}

			return tracks;
		}

		private static TrackDictionary LoadRepositoryInternal(string loadPath)
		{
			var savedTracks = new TrackDictionary();
			CoreHelper.WriteTraceEntry("Reading Repository XML...");

			XmlTextReader tr = null;
			try
			{
				tr = new XmlTextReader(loadPath);

				object track = tr.NameTable.Add("track");
				object feed = tr.NameTable.Add("feed");
				object categories = tr.NameTable.Add("categories");

				while (tr.Read())
				{
					if (tr.NodeType == XmlNodeType.Element && ReferenceEquals(tr.LocalName, categories))
					{
						CategoryManager.Deserialize(tr.ReadString());
					}

					if (tr.NodeType == XmlNodeType.Element && ReferenceEquals(tr.LocalName, track))
					{
						ReadTrack(tr, savedTracks);
					}

					if (tr.NodeType == XmlNodeType.Element && ReferenceEquals(tr.LocalName, feed))
					{
						ReadFeed(tr);
					}
				}
			}
			catch (Exception ex)
			{
				CoreHelper.WriteLogEntry(string.Format("Unable to read feed store from: {0}! Reason: {1}", loadPath, ex.Message));
				throw;
			}
			finally
			{
				if (tr != null)
					tr.Close();
			}

			CoreHelper.WriteTraceEntry("Reading completed!");
			return savedTracks;
		}

		/// <summary>
		/// Reads a track from the the XML stream
		/// </summary>
		/// <param name="tr"></param>
		private static void ReadTrack(XmlReader tr, TrackDictionary i_collection)
		{
			try
			{
				string name = String.Empty, path = String.Empty, protocol = string.Empty, url = String.Empty;
				double totalTime = 0, playedTime = 0;
				long downloadSize = 0, downloadPortion = 0;
				bool played = false;
				DateTime? pubDate = null;

				while (tr.MoveToNextAttribute())
				{
					switch (tr.LocalName)
					{
						case "path":
							path = tr.Value;
							break;

						case "name":
							name = tr.Value;
							break;

						case "totalTime":
							totalTime = TryParseDoubleFromString(tr.Value);
							break;

						case "playedTime":
							playedTime = TryParseDoubleFromString(tr.Value);
							break;

						case "protocol":
							protocol = tr.Value;
							break;

						case "played":
							played = tr.ReadContentAsBoolean();
							break;

						case "url":
							url = tr.Value;
							break;

						case "downloadSize":
							downloadSize = TryParseLongFromString(tr.Value);
							break;

						case "downloadPortion":
							downloadPortion = TryParseLongFromString(tr.Value);
							break;

						case "pubDate":
							pubDate = TryParseGMTDateTimeAsLocal(tr.Value);
							break;
					}
				}

				var t = new Track(path, null)
									{
										Name = name,
										TotalTime = totalTime,
										PlayedTime = playedTime,
										Protocol = protocol,
										Url = url,
										DownloadedPortion = downloadPortion,
										DownloadSize = downloadSize,
										LastModifiedDate = pubDate
									};

				if (played)
					t.MarkPlayed();

				i_collection.Add(t.TrackPath, t);
			}
			catch (Exception ex)
			{
				CoreHelper.WriteLogEntry("Unable to read track! " + ex.Message);
			}
		}

		/// <summary>
		/// Reads a feed from the the XML stream
		/// </summary>
		/// <param name="tr"></param>
		private static void ReadFeed(XmlReader tr)
		{
			try
			{
				string name = String.Empty,
							 path = String.Empty,
							 url = string.Empty,
							 imageUrl = string.Empty,
							 id = string.Empty,
							 category = string.Empty,
							 username = string.Empty,
							 password = string.Empty;

				int autodelete = 0, view = 0, type = 0, sortOrder = -1, feedPlayer = -1;
				int? maxTracks = null, maxTrackAge = null, maxDownload = null, maxWhatIsNew = null;

				bool isNew = false, hasUnread = false, custUpdate = false, forceUniqueNames = false, leftTruncateLongTrackNames = false, forceItemSort = false;
				DateTime? pubDate = null, srvPubDate = null;
				int? lastItemID = null;

				while (tr.MoveToNextAttribute())
				{
					switch (tr.LocalName)
					{
						case "id":
							id = tr.Value;
							break;

						case "path":
							path = tr.Value;
							break;

						case "name":
							name = tr.Value;
							break;

						case "url":
							url = tr.Value;
							break;

						case "imageUrl":
							imageUrl = tr.Value;
							break;

						case "category":
							category = tr.Value;
							break;

						case "autodelete":
							autodelete = tr.ReadContentAsInt();
							break;

						case "view":
							view = tr.ReadContentAsInt();
							break;

						case "type":
							type = tr.ReadContentAsInt();
							break;

						case "maxTracks":
							maxTracks = tr.ReadContentAsInt();
							break;

						case "maxTrackAge":
							maxTrackAge = tr.ReadContentAsInt();
							break;

						case "maxDownload":
							maxDownload = tr.ReadContentAsInt();
							break;

						case "numWhatIsNew":
							maxWhatIsNew = tr.ReadContentAsInt();
							break;

						case "isNew":
							isNew = tr.ReadContentAsBoolean();
							break;

						case "hasUnread":
							hasUnread = tr.ReadContentAsBoolean();
							break;

						case "custDownload":
							custUpdate = tr.ReadContentAsBoolean();
							break;

						case "forceUniqueNames":
							forceUniqueNames = tr.ReadContentAsBoolean();
							break;

						case "leftTruncateLongTrackNames":
							leftTruncateLongTrackNames = tr.ReadContentAsBoolean();
							break;

						case "trackSort":
							sortOrder = tr.ReadContentAsInt();
							break;

						case "feedPlayer":
							feedPlayer = tr.ReadContentAsInt();
							break;

						case "forceItemSort":
							forceItemSort = tr.ReadContentAsBoolean();
							break;

						case "pubDate":
							pubDate = TryParseGMTDateTimeAsLocal(tr.Value);
							break;

						case "srvPubDate":
							srvPubDate = TryParseGMTDateTimeAsLocal(tr.Value);
							break;

						case "lastItemId":
							lastItemID = tr.ReadContentAsInt();
							break;

						case "username":
							username = tr.Value;
							break;

						case "password":
							password = tr.Value;
							break;
					}
				}

				var loaded = new Feed(new Guid(id), RootFeed)
											{
												Name = name,
												FeedUrl = url,
												FeedImageUrl = imageUrl,
												AllowAutoTrackDeletions = ((AllowDeletions)autodelete),
												ViewType = ((FeedViewType)view),
												Type = ((FeedType)type),
												FeedPath = path,
												HasNewContent = isNew,
												HasUnreadItems = hasUnread,
												LastModifiedDate = pubDate,
												LastServerModifiedDate = srvPubDate,
												Categories = FeedCategories.Deserialize(category),
												IncludeInOneStepDownload = custUpdate,
												ForceFeedItemSort = forceItemSort,
												ForceUniqueTrackNames = forceUniqueNames,
												LeftTruncateLongTrackNames = leftTruncateLongTrackNames,
												LastItemID = lastItemID,
												FeedPlayer = ((PlayerIntegration)feedPlayer),
												UserName = username,
												Password = password
											};

				if (sortOrder != -1)
					loaded.TrackSortOrder = (CoreHelper.SortOrder)sortOrder;

				if (maxDownload.HasValue)
					loaded.MaxEnclosuresToOneStepDownload = maxDownload.Value;

				if (maxTracks.HasValue)
					loaded.MaxEnclosures = maxTracks.Value;

				if (maxTrackAge.HasValue)
					loaded.KeepEnclosuresDuration = new TimeSpan(maxTrackAge.Value, 0, 0, 0, 0);

				if (maxWhatIsNew.HasValue)
					loaded.NumToIncludeInWhatIsNew = maxWhatIsNew.Value;

				RootFeed.Feeds.Add(loaded);
				loaded.FeedChanged += OnFeedChanged;
			}
			catch (Exception ex)
			{
				CoreHelper.WriteLogEntry("Unable to read feed! " + ex.Message);
			}
		}

		private static void BuildFeedList(Feed i_parent, TrackDictionary i_SavedTracks)
		{
			i_parent.Feeds.Clear();

			if (String.IsNullOrEmpty(i_parent.FeedPath))
				return;

			//Load all subfeeds
			var d = new DirectoryInfo(i_parent.FeedPath);

			if (!d.Exists)
				return;

			//Load subdirectories only for feeds that don't have a URL
			if (!i_parent.HasUrl)
			{
				try
				{
					DirectoryInfo[] subfeeds = d.GetDirectories();
					foreach (DirectoryInfo di in subfeeds)
					{
						var newFeed = new Feed(di, i_parent);
						BuildFeedList(newFeed, i_SavedTracks);

						i_parent.Feeds.Add(newFeed);

						if (!i_parent.HasUrl && i_parent.LastModifiedDate < newFeed.LastModifiedDate)
							i_parent.LastModifiedDate = newFeed.LastModifiedDate;
					}
				}
				catch (Exception e)
				{
					CoreHelper.WriteLogEntry("Error loading feeds! " + e.Message);
				}
			}

			try
			{
				LoadFeedTracks(i_parent, i_SavedTracks);
			}
			catch (Exception e)
			{
				CoreHelper.WriteLogEntry(string.Format("Error loading podcasts for feed: {0}. Reason: {1}", i_parent.Name, e.Message));
			}

			SortTracks(i_parent);
			SortFeeds(i_parent);
		}

		private static void LoadFeedTracks(Feed i_parent, TrackDictionary i_SavedTracks)
		{
			i_parent.Tracks.Clear();

			if (String.IsNullOrEmpty(i_parent.FeedPath))
				return;

			var d = new DirectoryInfo(i_parent.FeedPath);

			if (!d.Exists)
				return;

			// Load all tracks
			FileInfo[] tracks = d.GetFiles("*.*");

			foreach (FileInfo fi in tracks)
			{
				//Skip the hidden files
				if ((fi.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
					continue;

				//Skip tracks that were already loaded by some other feed
				if (m_TrackDictionary.ContainsKey(fi.FullName))
					continue;

				var track = new Track(fi, i_parent);

				if (!track.SupportedFileType && !Configuration.ShowUnsupportedFileTypes)
					continue;

				AddChildTrack(i_parent, track, false);

				//See if we have a saved information about this track
				if (i_SavedTracks.ContainsKey(track.TrackPath))
				{
					Track saved = i_SavedTracks[track.TrackPath];
					track.Name = saved.Name;
					track.TotalTime = saved.TotalTime;
					track.PlayedTime = saved.PlayedTime;
					if (saved.IsPlayed)
						track.MarkPlayed();
					track.Protocol = saved.Protocol;

					if (saved.HasUrl)
					{
						//Make sure we have the real file size
						track.DownloadedPortion = new FileInfo(track.TrackPath).Length;
						track.DownloadSize = saved.DownloadSize;
						track.Url = saved.Url;
						if (!track.IsFullyDownloaded)
							track.DownloadStatus = Track.DownloadStatuses.Stopped;

						if (saved.LastModifiedDate.HasValue)
							track.LastModifiedDate = saved.LastModifiedDate;
					}
				}

				if (!i_parent.HasUrl && track.LastModifiedDate.HasValue &&
						(!i_parent.LastModifiedDate.HasValue || (i_parent.LastModifiedDate < track.LastModifiedDate)))
					i_parent.LastModifiedDate = track.LastModifiedDate;

				//Try to update the name of the track by parsing the ID3 tags of the file
				if (track.HasGeneratedName)
					track.LoadTrackNameFromFile();
			}
		}

		private static void AddChildTrack(Feed parent, Track child, bool forceResort)
		{
			parent.Tracks.Add(child);

			if (forceResort)
				SortTracks(parent);

			if (!m_TrackDictionary.ContainsKey(child.TrackPath))
				child.TrackChanged += OnTrackChanged;

			m_TrackDictionary[child.TrackPath] = child;
			OnTrackAdded(child);
		}

		private static void OnTrackChanged(Track aTrack)
		{
			if (TrackChanged != null)
				TrackChanged(aTrack);

			AutoSaveRepository();
		}

		private static void OnFeedChanged(Feed aFeed)
		{
			if (FeedChanged != null)
				FeedChanged(aFeed);

			AutoSaveRepository();
		}

		private static void DeleteTracksInternal(TrackList toDelete)
		{
			if (toDelete.Count == 0)
			{
				MessageBoxEx.Show(Resources.NothingToDeleteMsg, Resources.NothingToDeleteTitle, MessageBoxButtons.OK,
													MessageBoxIcon.Exclamation,
													MessageBoxDefaultButton.Button1);
				return;
			}

			var sb = new StringBuilder();
			toDelete.ForEach(
				t => sb.Append(t.Name + " - " +
											 (t.LastModifiedDate.HasValue ? ((DateTime)t.LastModifiedDate).ToShortDateString() : string.Empty) +
												"\r\n"));

			if (MessageBoxEx.Show(string.Format(Resources.AboutToDeleteMsg, toDelete.Count, sb),
														Resources.AboutToDeleteFileTitle, MessageBoxButtons.YesNo, MessageBoxIcon.Question,
														MessageBoxDefaultButton.Button1) == DialogResult.No)
				return;


			int numDeleted = 0;
			foreach (Track tr in toDelete)
			{
				if (DeleteTrackInternal(tr, false))
					numDeleted++;
			}

			MessageBoxEx.Show(string.Format(Resources.DeletedMsg, numDeleted, toDelete.Count), Resources.DeletedTitle,
												MessageBoxButtons.OK, MessageBoxIcon.Exclamation,
												MessageBoxDefaultButton.Button1);
		}

		private static bool DeleteTrackInternal(Track track, bool silent)
		{
			CoreHelper.WriteTraceEntry("Deleting track: " + track.Name + " ...");

			try
			{
				OnTrackBeforeDelete(track);
				track.Delete();
				m_TrackDictionary.Remove(track.TrackPath);
				track.ParentFeed.Tracks.Remove(track);
				OnTrackDeleted(track);
				return true;
			}
			catch (Exception ex)
			{
				CoreHelper.WriteLogEntry("Unable to delete track! Reason:" + ex.Message);

				if (!silent)
				{
					MessageBox.Show(string.Format(Resources.UnableToDeleteMsg, track.TrackPath,
																											track.WillUseInternalPlayer ?
																											string.Empty :
																											Resources.UnableToDeleteMsg1), Resources.UnableToDeleteTitle,
													MessageBoxButtons.OK, MessageBoxIcon.Hand,
													MessageBoxDefaultButton.Button1);
				}
				return false;
			}
		}

		private static void OnTrackBeforeDelete(Track i_track)
		{
			if (BeforeTrackDelete != null)
				BeforeTrackDelete(i_track);
		}

		private static void OnTrackDeleted(Track i_track)
		{
			if (TrackDeleted != null)
				TrackDeleted(i_track);
		}

		private static void OnTrackAdded(Track i_track)
		{
			if (!IsRepositoryLoading && TrackAdded != null)
				TrackAdded(i_track);
		}

		private static string LongToInvariantCultureString(long value)
		{
			return value.ToString(CultureInfo.InvariantCulture);
		}

		private static long TryParseLongFromString(string value)
		{
			if (string.IsNullOrEmpty(value))
				return 0;

			try
			{
				return long.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture);
			}
			catch
			{
				try
				{
					return long.Parse(value);
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry(string.Format("Unable to parse feed/track value of type long: [{0}]! Reason: {1}", value, ex.Message));
				}
			}

			return 0;
		}

		private static string DoubleToInvariantCultureString(double value)
		{
			return value.ToString("r", CultureInfo.InvariantCulture);
		}

		private static double TryParseDoubleFromString(string value)
		{
			if (string.IsNullOrEmpty(value))
				return 0;

			try
			{
				return Double.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture);
			}
			catch
			{
				try
				{
					return Double.Parse(value);
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry(string.Format("Unable to parse feed/track value of type double: [{0}]! Reason: {1}", value, ex.Message));
				}
			}

			return 0;
		}

		private static string LocalDateTimeToGMTString(DateTime? date)
		{
			return date.HasValue ? date.Value.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture) : string.Empty;
		}

		private static DateTime? TryParseGMTDateTimeAsLocal(string datetimeStr)
		{
			if (string.IsNullOrEmpty(datetimeStr))
				return null;

			try
			{
				return DateTime.ParseExact(datetimeStr, "r", CultureInfo.InvariantCulture).ToLocalTime();
			}
			catch
			{
				try
				{
					return DateTime.Parse(datetimeStr);
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry(string.Format("Unable to parse feed/track value of type date: [{0}]! Reason: {1}", datetimeStr, ex.Message));
				}
			}
			return null;
		}

		#endregion
	}
}